package git

import (
	"context"

	"gitee.com/go-errors/errors"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport"
	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
)

type (
	CloneOptions = git.CloneOptions
	AuthMethod   = transport.AuthMethod
	Repository   = git.Repository
)

func Open(path string) (repo *Repository, err error) {
	return git.PlainOpen(path)
}

func Clone(ctx context.Context, path string, isBare bool, o *CloneOptions) (repo *Repository, err error) {
	return git.PlainCloneContext(ctx, path, isBare, o)
}

func PublicKeyAuth(user string, pemBytes []byte, password string) (AuthMethod, error) {
	return ssh.NewPublicKeys(user, pemBytes, password)
}

func PasswordAuth(user string, password string) (AuthMethod, error) {
	cb, err := ssh.NewKnownHostsCallback()
	if err != nil {
		return nil, err
	}
	return &ssh.Password{
		User:                  user,
		Password:              password,
		HostKeyCallbackHelper: ssh.HostKeyCallbackHelper{HostKeyCallback: cb},
	}, nil
}

func PasswordCallbackAuth(user string, password func() (password string, err error)) (AuthMethod, error) {
	cb, err := ssh.NewKnownHostsCallback()
	if err != nil {
		return nil, err
	}
	return &ssh.PasswordCallback{
		User:                  user,
		Callback:              password,
		HostKeyCallbackHelper: ssh.HostKeyCallbackHelper{HostKeyCallback: cb},
	}, nil
}

func getDescribe(repo *Repository) (describe Describe, err error) {
	var head *plumbing.Reference
	if head, err = repo.Head(); err != nil {
		err = errors.Errorf("unable to find head: %v", err)
		return
	}
	describe.Hash = Hash(head.Hash())

	if head.Name().IsTag() {
		describe.Tag = head.Name().Short()
		return
	}

	if head.Name().IsBranch() {
		describe.Branch = head.Name().Short()
	}

	var tags map[string]string
	if tags, err = getTagMap(repo); err != nil {
		err = errors.Errorf("unable to get tags: %v", err)
		return
	}

	var commitIter object.CommitIter
	commitIterOpt := &git.LogOptions{From: head.Hash(), Order: git.LogOrderBSF}
	if commitIter, err = repo.Log(commitIterOpt); err != nil {
		err = errors.Errorf("unable to get log: %v", err)
		return
	}

	type gitDescribeNode struct {
		Commit   object.Commit
		Distance int
	}

	state := map[string]gitDescribeNode{}
	tagHash := ""
	_ = commitIter.ForEach(func(c *object.Commit) error {
		node, found := state[c.Hash.String()]
		if !found {
			node = gitDescribeNode{
				Commit:   *c,
				Distance: 0,
			}
			state[c.Hash.String()] = node
		}
		_ = c.Parents().ForEach(func(p *object.Commit) error {
			_, found := state[p.Hash.String()]
			if !found {
				state[p.Hash.String()] = gitDescribeNode{
					Commit:   *p,
					Distance: node.Distance + 1,
				}
			}
			return nil
		})

		_, foundTag := tags[c.Hash.String()]
		if tagHash == "" && foundTag {
			describe.Counter = state[c.Hash.String()].Distance
			tagHash = c.Hash.String()
		}
		return nil
	})

	if tagHash == "" {
		for _, node := range state {
			if node.Distance+1 > describe.Counter {
				describe.Counter = node.Distance + 1
			}
		}
		return
	}

	describe.Tag = tags[tagHash]
	return
}

func getTagMap(repo *Repository) (map[string]string, error) {
	iter, err := repo.Tags()
	if err != nil {
		return nil, err
	}
	tagMap := map[string]string{}
	err = iter.ForEach(func(r *plumbing.Reference) error {
		tag, _ := repo.TagObject(r.Hash())
		if tag == nil {
			tagMap[r.Hash().String()] = r.Name().Short()
		} else {
			c, err := tag.Commit()
			if err != nil {
				return err
			}
			tagMap[c.Hash.String()] = r.Name().Short()
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return tagMap, nil
}
